{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Optimal Power Flow via PandaModels\n",
    "### PandaModels.jl: Interfacing PowerModels with pandapower\n",
    "\n",
    "This tutorial describes how to run the Optimal Power Flow via [PandaModels.jl](https://e2niee.github.io/PandaModels.jl/dev/) calling [PowerModels.jl](https://lanl-ansi.github.io/PowerModels.jl/stable/) package."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Let's get started\n",
    "\n",
    "So here is an example of how it works. First, we create a grid in pandapower. Here, we create a meshed 110kV grid with four buses that is fed from an 220kV network through a 3-Winding transformer.\n",
    "Also we need to set min/max values as optimization constarints."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pandapower.create import (\n",
    "    create_empty_network,\n",
    "    create_bus,\n",
    "    create_transformer3w_from_parameters,\n",
    "    create_line,\n",
    "    create_gen,\n",
    "    create_load,\n",
    "    create_poly_cost\n",
    ")\n",
    "from pandapower.runpm import runpm_ac_opf, runpm_dc_opf\n",
    "from pandapower.run import runopp"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net = create_empty_network()\n",
    "\n",
    "min_vm_pu = 0.95\n",
    "max_vm_pu = 1.05\n",
    "\n",
    "#create buses\n",
    "bus1 = create_bus(net, vn_kv=220., geodata=(5,9), min_vm_pu=min_vm_pu, max_vm_pu=max_vm_pu)\n",
    "bus2 = create_bus(net, vn_kv=110., geodata=(6,10), min_vm_pu=min_vm_pu, max_vm_pu=max_vm_pu)\n",
    "bus3 = create_bus(net, vn_kv=110., geodata=(10,9), min_vm_pu=min_vm_pu, max_vm_pu=max_vm_pu)\n",
    "bus4 = create_bus(net, vn_kv=110., geodata=(8,8), min_vm_pu=min_vm_pu, max_vm_pu=max_vm_pu)\n",
    "bus5 = create_bus(net, vn_kv=110., geodata=(6,8), min_vm_pu=min_vm_pu, max_vm_pu=max_vm_pu)\n",
    "\n",
    "#create 220/110/110 kV 3W-transformer\n",
    "create_transformer3w_from_parameters(net, bus1, bus2, bus5, vn_hv_kv=220, vn_mv_kv=110,\n",
    "                                        vn_lv_kv=110, vk_hv_percent=10., vk_mv_percent=10.,\n",
    "                                        vk_lv_percent=10., vkr_hv_percent=0.5,\n",
    "                                        vkr_mv_percent=0.5, vkr_lv_percent=0.5, pfe_kw=10,\n",
    "                                        i0_percent=0.1, shift_mv_degree=0, shift_lv_degree=0,\n",
    "                                        sn_hv_mva=100, sn_mv_mva=50, sn_lv_mva=50)\n",
    "\n",
    "#create 110 kV lines\n",
    "l1 = create_line(net, bus2, bus3, length_km=70., std_type='149-AL1/24-ST1A 110.0')\n",
    "l2 = create_line(net, bus3, bus4, length_km=50., std_type='149-AL1/24-ST1A 110.0')\n",
    "l3 = create_line(net, bus4, bus2, length_km=40., std_type='149-AL1/24-ST1A 110.0')\n",
    "l4 = create_line(net, bus4, bus5, length_km=30., std_type='149-AL1/24-ST1A 110.0')\n",
    "\n",
    "#create loads\n",
    "create_load(net, bus2, p_mw=60)\n",
    "create_load(net, bus3, p_mw=70)\n",
    "create_load(net, bus4, p_mw=10)\n",
    "\n",
    "#create generators\n",
    "g1 = create_gen(net, bus1, p_mw=40, min_p_mw=0, max_p_mw=200, vm_pu=1.01, slack=True)\n",
    "create_poly_cost(net, g1, 'gen', cp1_eur_per_mw=1)\n",
    "\n",
    "g2 = create_gen(net, bus3, p_mw=40, min_p_mw=0, max_p_mw=200, vm_pu=1.01)\n",
    "create_poly_cost(net, g2, 'gen', cp1_eur_per_mw=3)\n",
    "\n",
    "g3 = create_gen(net, bus4, p_mw=50, min_p_mw=0, max_p_mw=200, vm_pu=1.01)\n",
    "create_poly_cost(net, g3, 'gen', cp1_eur_per_mw=3)\n",
    "net"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that PowerModels does not have a 3W-transformer model, but since pandapower includes the equations to calculate the equivalent branches for the 3W-transformers (2W-transformers instead), it is possible to optimize grids with 3W-transformers in PowerModels through the pandapower interface. The same is true for other complex transformer models, switches/breaker, extended ward equivalents etc.\n",
    "\n",
    "Let's have a look at the grid we created with pandapowers plotting module:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "import geojson\n",
    "geojson.loads(net.bus.geo.iloc[0])[\"coordinates\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandapower.plotting as plot\n",
    "plot.set_line_geodata_from_bus_geodata(net)\n",
    "plot.simple_plot(net)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's run an OPF through PowerModels and look at the results (Note that the first time the runpm function is called, Julia is started in the background, which may take some time):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    runpm_ac_opf(net)\n",
    "except Exception as err:\n",
    "    print(err)\n",
    "# or: runpm(net, pm_model=\"ACPPowerModel\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "Also, there are further parameters and options that you can add as input while calling the Optimization Problem from PandaModles:\n",
    "\n",
    "| parameter | description | type | default |\n",
    "| :--- | :--- | :---: | :--- |\n",
    "| correct_pm_network_data | checks if network data is correct. If not tries to correct it | bool | True |\n",
    "| silence | Suppresses information and warning messages output by PowerModels | bool | True |\n",
    "| pm_model | PowerModels.jl model to use | str | \"ACPPowerModel\" |\n",
    "| pm_solver | \"main\" solver| str | \"ipopt\" |\n",
    "| pm_mip_solver | mixed integer solver| str | \"cbc\" |\n",
    "| pm_nl_solver | nonlinear solver| str | \"ipopt\" |\n",
    "| pm_tol | default desired convergence tolerance for solver to use | float | 1e-8 |\n",
    "| pm_log_level | solver log level in power models | int | 0 |\n",
    "| delete_buffer_file | If True, the .json file used by PandaModels will be deleted after optimization. | bool | True |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since Generator 1 has the lowest cost, all required power is supplied through this generator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.res_gen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This however leeds to an overload in the three-winding transformer, through which g1 is connected:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.res_trafo3w.loading_percent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's set some constraints for the 3W-transformer and the lines and rerun the OPF:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.trafo3w[\"max_loading_percent\"] = 50\n",
    "net.line[\"max_loading_percent\"] = 20\n",
    "try:\n",
    "    runpm_ac_opf(net)\n",
    "except:\n",
    "    print(\"Cannot be performed due to [WinError 3] - Can't find file python39.dll\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The constraints are complied with for all lines and the 3W transformer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.res_trafo3w.loading_percent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.res_line.loading_percent"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The power is now generated by a mixture of the generators:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.res_gen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Accessing the full functionality of PowerModels.jl"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Apart from the AC OPF used in the example above, pandapower also has an interface to run the DC OPF:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    runpm_dc_opf(net)\n",
    "except:\n",
    "    print(\"Cannot be performed due to [WinError 3] - Can't find file python39.dll\")\n",
    "# or: runpm(net, pm_model=\"DCPPowerModel\")\n",
    "net.res_bus\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The PowerModels data structure that was passed to Julia can be accessed like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "net._pm[\"bus\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Timings"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Comparing the runopp function (that runs an OPF through PYPOWER) and the runpm function shows that PandaModels.jl is much more performant:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%timeit runopp(net)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    %timeit runpm_ac_opf(net)\n",
    "except:\n",
    "    print(\"Cannot be performed due to [WinError 3] - Can't find file python39.dll\")"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
